<template>
  <div
    ref="treeWrap"
    :class="prefixCls"
  >
    <Tree-node
      v-for="(item, i) in stateTree"
      :key="i"
      :data="item"
      visible
      :multiple="multiple"
      :show-checkbox="showCheckbox"
      :children-key="childrenKey"
    />
    <div
      v-if="!stateTree.length"
      :class="[prefixCls + '-empty']"
    >
      {{ localeEmptyText }}
    </div>
    <div
      class="ivu-tree-context-menu"
      :style="contextMenuStyles"
    >
      <Dropdown
        trigger="custom"
        :visible="contextMenuVisible"
        transfer
        @on-clickoutside="handleClickContextMenuOutside"
      >
        <DropdownMenu slot="list">
          <slot name="contextMenu" />
        </DropdownMenu>
      </Dropdown>
    </div>
  </div>
</template>
<script>
import TreeNode from './node.vue'
import Dropdown from '../dropdown/dropdown.vue'
import DropdownMenu from '../dropdown/dropdown-menu.vue'
import Emitter from '../../mixins/emitter'
import Locale from '../../mixins/locale'

const prefixCls = 'ivu-tree'

export default {
  name: 'Tree',
  components: { TreeNode, Dropdown, DropdownMenu },
  mixins: [Emitter, Locale],
  provide () {
    return { TreeInstance: this }
  },
  props: {
    data: {
      type: Array,
      default () {
        return []
      }
    },
    multiple: {
      type: Boolean,
      default: false
    },
    showCheckbox: {
      type: Boolean,
      default: false
    },
    checkStrictly: {
      type: Boolean,
      default: false
    },
    // 当开启 showCheckbox 时，如果开启 checkDirectly，select 将强制转为 check 事件
    checkDirectly: {
      type: Boolean,
      default: false
    },
    emptyText: {
      type: String
    },
    childrenKey: {
      type: String,
      default: 'children'
    },
    loadData: {
      type: Function
    },
    render: {
      type: Function
    },
    selectNode: {
      type: Boolean,
      default: true
    },
    expandNode: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      prefixCls: prefixCls,
      stateTree: this.data,
      flatState: [],
      contextMenuVisible: false,
      contextMenuStyles: {
        top: 0,
        left: 0
      }
    }
  },
  computed: {
    localeEmptyText () {
      if (typeof this.emptyText === 'undefined') {
        return this.t('i.tree.emptyText')
      } else {
        return this.emptyText
      }
    }
  },
  watch: {
    data: {
      deep: true,
      handler () {
        this.stateTree = this.data
        this.flatState = this.compileFlatState()
        this.rebuildTree()
      }
    }
  },
  created () {
    this.flatState = this.compileFlatState()
    this.rebuildTree()
  },
  mounted () {
    this.$on('on-check', this.handleCheck)
    this.$on('on-selected', this.handleSelect)
    this.$on('toggle-expand', node => this.$emit('on-toggle-expand', node))
    this.$on('contextmenu', this.handleContextmenu)
  },
  methods: {
    compileFlatState () { // so we have always a relation parent/children of each node
      let keyCounter = 0
      const childrenKey = this.childrenKey
      const flatTree = []
      function flattenChildren (node, parent) {
        node.nodeKey = keyCounter++
        flatTree[node.nodeKey] = { node: node, nodeKey: node.nodeKey }
        if (typeof parent !== 'undefined') {
          flatTree[node.nodeKey].parent = parent.nodeKey
          flatTree[parent.nodeKey][childrenKey].push(node.nodeKey)
        }

        if (node[childrenKey]) {
          flatTree[node.nodeKey][childrenKey] = []
          node[childrenKey].forEach(child => flattenChildren(child, node))
        }
      }
      this.stateTree.forEach(rootNode => {
        flattenChildren(rootNode)
      })
      return flatTree
    },
    updateTreeUp (nodeKey) {
      const parentKey = this.flatState[nodeKey].parent
      if (typeof parentKey === 'undefined' || this.checkStrictly) return

      const node = this.flatState[nodeKey].node
      const parent = this.flatState[parentKey].node
      if (node.checked == parent.checked && node.indeterminate == parent.indeterminate) return // no need to update upwards

      if (node.checked == true) {
        parent.checked = parent[this.childrenKey].every(node => node.checked)
        parent.indeterminate = !parent.checked
      } else {
        parent = checked = false
        parent = indeterminate = parent[this.childrenKey].some(node => node.checked || node.indeterminate)
      }
      this.updateTreeUp(parentKey)
    },
    rebuildTree () { // only called when `data` prop changes
      const checkedNodes = this.getCheckedNodes()
      checkedNodes.forEach(node => {
        this.updateTreeDown(node, { checked: true })
        // propagate upwards
        const parentKey = this.flatState[node.nodeKey].parent
        if (!parentKey && parentKey !== 0) return
        const parent = this.flatState[parentKey].node
        const childHasCheckSetter = typeof node.checked !== 'undefined' && node.checked
        if (childHasCheckSetter && parent.checked != node.checked) {
          this.updateTreeUp(node.nodeKey) // update tree upwards
        }
      })
    },

    getSelectedNodes () {
      /* public API */
      return this.flatState.filter(obj => obj.node.selected).map(obj => obj.node)
    },
    getCheckedNodes () {
      /* public API */
      return this.flatState.filter(obj => obj.node.checked).map(obj => obj.node)
    },
    getCheckedAndIndeterminateNodes () {
      /* public API */
      return this.flatState.filter(obj => (obj.node.checked || obj.node.indeterminate)).map(obj => obj.node)
    },
    updateTreeDown (node, changes = {}) {
      if (this.checkStrictly) return

      for (const key in changes) {
        node[key] = changes[key]
      }
      if (node[this.childrenKey]) {
        node[this.childrenKey].forEach(child => {
          this.updateTreeDown(child, changes)
        })
      }
    },
    handleSelect (nodeKey) {
      if (!this.flatState[nodeKey]) return
      const node = this.flatState[nodeKey].node
      if (!this.multiple) { // reset previously selected node
        const currentSelectedKey = this.flatState.findIndex(obj => obj.node.selected)

        if (currentSelectedKey >= 0 && currentSelectedKey !== nodeKey) {
          this.flatState[currentSelectedKey].node.selected = false
        }
      }
      node.selected = !node.selected

      this.$emit('on-select-change', this.getSelectedNodes(), node)
    },
    handleCheck ({ checked, nodeKey }) {
      if (!this.flatState[nodeKey]) return
      const node = this.flatState[nodeKey].node
      node.checked = checked
      node.indeterminate = false

      this.updateTreeUp(nodeKey) // propagate up
      this.updateTreeDown(node, { checked, indeterminate: false }) // reset `indeterminate` when going down

      this.$emit('on-check-change', this.getCheckedNodes(), node)
    },
    handleContextmenu ({ data, event }) {
      if (this.contextMenuVisible) this.handleClickContextMenuOutside()
      this.$nextTick(() => {
        const $TreeWrap = this.$refs.treeWrap
        const TreeBounding = $TreeWrap.getBoundingClientRect()
        const position = {
          left: `${event.clientX - TreeBounding.left}px`,
          top: `${event.clientY - TreeBounding.top}px`
        }
        this.contextMenuStyles = position
        this.contextMenuVisible = true
        this.$emit('on-contextmenu', data, event, position)
      })
    },
    handleClickContextMenuOutside () {
      this.contextMenuVisible = false
    }
  }
}
</script>
